package ch.cryptobox.serivces;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Date;

import org.acplt.oncrpc.OncRpcException;
import org.acplt.oncrpc.OncRpcProtocols;

import ch.cryptobox.Globals;
import ch.cryptobox.filehandles.FileHandle;
import ch.cryptobox.filehandles.FileHandleI;
import ch.cryptobox.mount.dirpath;
import ch.cryptobox.mount.fhstatus;
import ch.cryptobox.mount.mountClient;
import ch.cryptobox.nfsError.CreateFileException;
import ch.cryptobox.nfsError.FileNotFoundException;
import ch.cryptobox.nfsError.LookupException;
import ch.cryptobox.nfsError.ReadWriteException;
import ch.cryptobox.nfsclient.attrstat;
import ch.cryptobox.nfsclient.createargs;
import ch.cryptobox.nfsclient.diropargs;
import ch.cryptobox.nfsclient.diropres;
import ch.cryptobox.nfsclient.entry;
import ch.cryptobox.nfsclient.fhandle;
import ch.cryptobox.nfsclient.filename;
import ch.cryptobox.nfsclient.nfsClient;
import ch.cryptobox.nfsclient.nfscookie;
import ch.cryptobox.nfsclient.nfsdata;
import ch.cryptobox.nfsclient.readargs;
import ch.cryptobox.nfsclient.readdirargs;
import ch.cryptobox.nfsclient.readdirres;
import ch.cryptobox.nfsclient.readdirresOK;
import ch.cryptobox.nfsclient.readres;
import ch.cryptobox.nfsclient.sattr;
import ch.cryptobox.nfsclient.timeval;
import ch.cryptobox.nfsclient.writeargs;

public class NFSService implements SynchronizationService {

	private final String localMountPath; 
	private final mountClient mount;
	private final String nfsRoot;
	private nfsClient client;
	private fhandle rootDir;
	private final int id;


	//	public NFSService(final String serverURL, final String path) throws UnknownHostException, OncRpcException, IOException
	//	{
	//		this(serverURL, path,  Globals.DEFAULT_ROOT);
	//		
	////		System.out.println(serverURL);
	////		System.out.println(localMountPath);
	////		System.out.println(path);
	//	}


	public NFSService(final String serverURL, final String path, final String localMountPath) throws UnknownHostException, OncRpcException, IOException
	{
		this(serverURL,path,localMountPath,0);
	}
	
	
	public NFSService(final String serverURL, final String path, final String localMountPath, final int id, final int port) throws UnknownHostException, OncRpcException, IOException
	{
		this.id=id;
		this.localMountPath = localMountPath;
		nfsRoot=path;
		mount = new mountClient(InetAddress.getByName(serverURL), OncRpcProtocols.ONCRPC_UDP);
		
		final fhstatus tmp = mount.MOUNTPROC_MNT_1(new dirpath(nfsRoot));
		if(tmp.status == 0){
			client = new nfsClient(InetAddress.getByName(serverURL), OncRpcProtocols.ONCRPC_UDP, port);

			rootDir=new fhandle(tmp.directory.value);
		} else {
			System.out.println("BAD INIT of " + NFSService.class.getName());
		}

		//final OncRpcClientAuth auth = new OncRpcClientAuthUnix("pat.local",1005,50);
		//mnt.getClient().setAuth(auth);
	}
	

	public NFSService(final String serverURL, final String path, final String localMountPath, final int id) throws UnknownHostException, OncRpcException, IOException
	{
		this.id=id;
		this.localMountPath = localMountPath;
		nfsRoot=path;
		mount = new mountClient(InetAddress.getByName(serverURL), OncRpcProtocols.ONCRPC_UDP);
		
		final fhstatus tmp = mount.MOUNTPROC_MNT_1(new dirpath(nfsRoot));
		if(tmp.status == 0){
			client = new nfsClient(InetAddress.getByName(serverURL), OncRpcProtocols.ONCRPC_UDP);
			
			rootDir=new fhandle(tmp.directory.value);
		} else {
			System.out.println("BAD INIT of " + NFSService.class.getName());
		}

		//final OncRpcClientAuth auth = new OncRpcClientAuthUnix("pat.local",1005,50);
		//mnt.getClient().setAuth(auth);
	}


	synchronized public fhandle getRootDir()
	{
		return rootDir;
	}


	private String[] getCryptoBoxRelativePath(final File file)
	{
		return file.getAbsolutePath().replaceAll(localMountPath, "").split(File.separator);
	}


	synchronized public void addDirectory(final File file) throws OncRpcException, IOException, CreateFileException, ReadWriteException, FileNotFoundException
	{
		
		
		final String[] path=getCryptoBoxRelativePath(file);

		final fhandle parent=getParentForPath(rootDir, path, true);

		final String folderName=path[path.length-1];

		final diropres alreadyExists = lookup(parent, folderName);

		if(alreadyExists.status==2) {
			final BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
			final long lastAcc = attr.lastAccessTime().toMillis();
			final long lastMod = attr.lastModifiedTime().toMillis();
			createDirectory(parent, folderName, getTime(lastAcc), getTime(lastMod));

		}
	}

	synchronized public void addFile(final File file, final byte[] data) throws OncRpcException, IOException, CreateFileException, ReadWriteException, FileNotFoundException
	{
		//System.out.println(NFSService.class.getName() + ".addFile() Called at" + nfsRoot);

		final String[] path=getCryptoBoxRelativePath(file);

		final fhandle parent=getParentForPath(rootDir, path, true);

		final String fileName=path[path.length-1];

		final diropres alreadyExists = lookup(parent, fileName);

		fhandle fhandle;
		if(alreadyExists.status!=2)
			fhandle=alreadyExists.diropok.file;
		else {
			final BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
			final long lastAcc = attr.lastAccessTime().toMillis();
			final long lastMod = attr.lastModifiedTime().toMillis();
			fhandle=createFile(parent, fileName, getTime(lastAcc), getTime(lastMod));

		}
		writeOnFile(fhandle, data);
	}

	//	synchronized public byte[] readFile(final File file) throws OncRpcException, IOException, CreateFileException, ReadWriteException, FileNotFoundException
	//	{
	//		final String[] path=getCryptoBoxRelativePath(file);
	//
	//		final fhandle parent=getParentForPath(rootDir, path, true);
	//
	//		final String fileName=path[path.length-1];
	//
	//		final diropres fhandle = lookup(parent, fileName);
	//
	//		if(fhandle.status!=0)
	//			throw new ReadWriteException(fhandle.status);
	//
	//		return readFile(fhandle.diropok.file);
	//	}


	private timeval getTime(final long millis) {
		final timeval t = new timeval();
		t.seconds = (int) (millis/1000.0);
		t.useconds = 0;
		return t;
	}

	private timeval now() {
		return getTime( new Date().getTime());
	}

	private fhandle getParentForPath(final fhandle from, final String[] path, final boolean createDire) throws OncRpcException, IOException, CreateFileException, FileNotFoundException
	{
		fhandle parent = from;

		for(int i=0;i<path.length-1;++i)
		{
			final String s=path[i];

			if(s.length()<=0) continue;

			final diropres res=lookup(parent, s);


			if(res.status==0) //no problem
			{
				parent=res.diropok.file;
				continue;
			}

			if(createDire && res.status==2) //The file or directory specified does not exist.
			{
				parent=createDirectory(parent, s, now(), now());
				continue;
			}
			if(createDire)
				throw new CreateFileException(s,res.status);
			else
				throw new FileNotFoundException(s);
		}

		return parent;
	}

	synchronized public diropres lookup(final fhandle parent,final String name) throws OncRpcException, IOException
	{
		final String[] split=name.split(File.separator);

		try {
			final fhandle fhandle=getParentForPath(parent, split, false);


			final filename filename=new filename();
			filename.value=split[split.length-1];

			final diropargs diropargs=new diropargs();
			diropargs.dir=fhandle;
			diropargs.name=filename;

			return client.NFSPROC_LOOKUP_2(diropargs);

		} catch (CreateFileException | FileNotFoundException e) {
			final diropres res=new diropres();
			res.status=1;
			return res;
		}

	}

	synchronized public diropres changeDir(final fhandle from, final String path) throws OncRpcException, IOException, CreateFileException, FileNotFoundException
	{
		final String[] split=path.split(File.separator);
		final fhandle parent=getParentForPath(from, split, false);

		final String folderName=split[split.length-1];

		final diropres dir = lookup(parent, folderName);

		if(dir.status!=2){
			if(dir.diropok.attributes.type!=2) //not a dir
				throw new FileNotFoundException(path);
			return dir;

		}
		throw new FileNotFoundException(path);
	}

	synchronized public ArrayList<String> readDir() throws LookupException, OncRpcException, IOException
	{
		return readDir(rootDir);
	}

	synchronized public ArrayList<String> readDir(final fhandle dir) throws LookupException, OncRpcException, IOException
	{
		final ArrayList<String> entries=new ArrayList<String>();

		final readdirargs r = new readdirargs();
		r.dir =  dir;
		final nfscookie cookie = new nfscookie(new byte[]{0,0,0,0});
		r.cookie = cookie;
		r.count = Globals.MAX_BYTES;

		final readdirres result = client.NFSPROC_READDIR_2(r);
		if(result.status==0)
		{
			final readdirresOK dirs = result.readdirok;
			entry entry = dirs.entries;
			while(dirs.eof)
			{
				if(entry==null)
					break;

				final String name=entry.name.value;
				entry=entry.nextentry;

				if(".".equals(name) || "..".equals(name))
					continue;

				//				final diropres tmp = lookup(dir, name);
				//
				//				if(tmp.diropok.attributes.type==2)
				//					name+="/";

				entries.add(name);

			}
		}
		else
			throw new LookupException(result.status+"");

		return entries;
	}

	synchronized private fhandle createDirectory(final fhandle parent, final String name, final timeval atime, final timeval mtime) throws OncRpcException, IOException, CreateFileException
	{
		final filename filename=new filename(name);
		final diropargs diropargs=new diropargs();
		diropargs.dir=parent;
		diropargs.name=filename;

		final sattr sattr=new sattr();
		sattr.mode=16895;
		sattr.uid=65534;
		sattr.gid=65534; //nfs anonymous
		sattr.atime=atime;
		sattr.mtime=mtime;


		/** A "size" of zero means the file should be truncated.
		 *  A value of -1 indicates a field that should be ignored.*/
		sattr.size=-1;

		final createargs createargs=new createargs();
		createargs.where=diropargs;
		createargs.attributes=sattr;

		final diropres result = client.NFSPROC_MKDIR_2(createargs);

		if(result.status!=0)
			throw new CreateFileException(name,result.status);

		return result.diropok.file;
	}

	synchronized private fhandle createFile(final fhandle parent, final String name, final timeval atime, final timeval mtime) throws OncRpcException, IOException, CreateFileException
	{
		final filename filename=new filename(name);
		final diropargs diropargs=new diropargs();
		diropargs.dir=parent;
		diropargs.name=filename;

		final sattr sattr=new sattr();
		sattr.mode=33279;
		sattr.uid=65534;
		sattr.gid=65534; //nfs anonymous
		sattr.atime=atime;
		sattr.mtime=mtime;


		/** A "size" of zero means the file should be truncated.
		 *  A value of -1 indicates a field that should be ignored.*/
		sattr.size=-1;

		final createargs createargs=new createargs();
		createargs.where=diropargs;
		createargs.attributes=sattr;

		final diropres result = client.NFSPROC_CREATE_2(createargs);

		if(result.status!=0)
			throw new CreateFileException(name,result.status);

		return result.diropok.file;
	}

	synchronized private void writeOnFile(final fhandle fhandle, final byte[] value) throws OncRpcException, IOException, ReadWriteException
	{
		final nfsdata data=new nfsdata();
		data.value=value;

		final writeargs writeargs=new writeargs();

		writeargs.file=fhandle;
		writeargs.data=data;
		writeargs.offset=0;

		/** The arguments "beginoffset" and "totalcount" are ignored and are removed in the next protocol revision.*/
		writeargs.beginoffset=0;
		writeargs.totalcount=0;

		final attrstat result = client.NFSPROC_WRITE_2(writeargs);

		if(result.status!=0)
			throw new ReadWriteException(result.status);
	}

	synchronized public byte[] readFile(final FileHandleI fhandle) throws OncRpcException, IOException, ReadWriteException
	{
		final readargs readargs=new readargs();

		readargs.file=((FileHandle)fhandle).getHandle();
		readargs.offset=0;
		readargs.count= Globals.MAX_BYTES;

		/**  The argument "totalcount" is unused, and is removed in the next protocol revision.*/
		readargs.totalcount=0;

		final readres result = client.NFSPROC_READ_2(readargs);

		if(result.status!=0)
			throw new ReadWriteException(result.status);

		return result.read.data.value;
	}


	@Override
	synchronized public FileHandleI getRootHandle() {
		return new FileHandle(this);
	}

	@Override
	public int getId() {
		return id;
	}
}
